/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.itsaky.androidide.layoutlib.resources;

import static com.google.common.base.MoreObjects.firstNonNull;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.Collection;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * Enum representing a type of compiled resource.
 *
 * <p>See {@code ResourceType} in aapt2/Resource.h.
 */
public enum ResourceType {
  ANIM("anim", "Animation"),
  ANIMATOR("animator", "Animator"),
  ARRAY("array", "Array", "string-array", "integer-array"),
  ATTR("attr", "Attr"),
  BOOL("bool", "Boolean"),
  COLOR("color", "Color"),
  DIMEN("dimen", "Dimension"),
  DRAWABLE("drawable", "Drawable"),
  FONT("font", "Font"),
  FRACTION("fraction", "Fraction"),
  ID("id", "ID"),
  INTEGER("integer", "Integer"),
  INTERPOLATOR("interpolator", "Interpolator"),
  LAYOUT("layout", "Layout"),
  MENU("menu", "Menu"),
  MIPMAP("mipmap", "Mip Map"),
  NAVIGATION("navigation", "Navigation"),
  PLURALS("plurals", "Plurals"),
  RAW("raw", "Raw"),
  STRING("string", "String"),
  STYLE("style", "Style"),
  STYLEABLE("styleable", "Styleable", Kind.STYLEABLE),
  TRANSITION("transition", "Transition"),
  XML("xml", "XML"),

  /**
   * This is not actually used. Only there because they get parsed and since we want to detect new
   * resource type, we need to have this one exist.
   */
  PUBLIC("public", "Public visibility modifier", Kind.SYNTHETIC),

  /**
   * This type is used for elements dynamically generated by the parsing of aapt:attr nodes. The
   * "aapt:attr" allow to inline resources as part of a different resource, for example, a drawable
   * as part of a layout. When the parser, encounters one of this nodes, it will generate a
   * synthetic _aapt attr reference.
   */
  AAPT("_aapt", "Aapt Attribute", Kind.SYNTHETIC),

  /**
   * This tag is used for marking a resource overlayable, i.e. that it can be overlaid at runtime by
   * RROs (Runtime Resource Overlays). This is a new feature supported starting Android 10. This tag
   * (and the content following it in that node) does not define a resource.
   */
  OVERLAYABLE("overlayable", "Overlayable tag", Kind.SYNTHETIC),

  /**
   * Represents item tags inside a style definition.
   */
  STYLE_ITEM("item", "Style Item", Kind.SYNTHETIC),

  /**
   * Not an actual resource type from AAPT. Used to provide sample data values in the tools
   * namespace
   */
  SAMPLE_DATA("sample", "Sample data", Kind.SYNTHETIC),

  /**
   * Not a real resource, but a way of defining a resource reference that will be replaced with its
   * actual value during linking. Does not exist at runtime, nor does it appear in the R class. Only
   * present in raw and flat resources.
   */
  MACRO("macro", "Macro resource replacement", Kind.SYNTHETIC),
  ;

  /**
   * The set of all types of resources that can be referenced by other resources.
   */
  public static final ImmutableSet<ResourceType> REFERENCEABLE_TYPES;
  private static final ImmutableMap<String, ResourceType> TAG_NAMES;
  private static final ImmutableMap<String, ResourceType> CLASS_NAMES;

  static {
    ImmutableMap.Builder<String, ResourceType> tagNames = ImmutableMap.builder();
    tagNames.put(AndroidConstants.TAG_DECLARE_STYLEABLE, STYLEABLE);
    tagNames.put(AndroidConstants.TAG_PUBLIC, PUBLIC);
    tagNames.put(OVERLAYABLE.getName(), OVERLAYABLE);
    tagNames.put(MACRO.getName(), MACRO);

    ImmutableMap.Builder<String, ResourceType> classNames = ImmutableMap.builder();
    classNames.put(STYLEABLE.mName, STYLEABLE);

    for (ResourceType type : ResourceType.values()) {
      if (type.mKind != Kind.REAL || type == STYLEABLE) {
        continue;
      }
      classNames.put(type.getName(), type);
      tagNames.put(type.getName(), type);
      for (String alternateName : type.mAlternateXmlNames) {
        tagNames.put(alternateName, type);
      }
    }

    TAG_NAMES = tagNames.build();
    CLASS_NAMES = classNames.build();

    final ImmutableSet.Builder<ResourceType> setBuilder = ImmutableSet.builder();
    for (ResourceType type : values()) {
      if (type.getCanBeReferenced()) {
        setBuilder.add(type);
      }
    }

    REFERENCEABLE_TYPES = setBuilder.build();
  }

  @NonNull
  private final String mName;
  @NonNull
  private final Kind mKind;
  @NonNull
  private final String mDisplayName;
  @NonNull
  private final String[] mAlternateXmlNames;

  ResourceType(
      @NonNull String name, @NonNull String displayName, @NonNull String... alternateXmlNames) {
    mName = name;
    mKind = Kind.REAL;
    mDisplayName = displayName;
    mAlternateXmlNames = alternateXmlNames;
  }

  ResourceType(@NonNull String name, @NonNull String displayName, @NonNull Kind kind) {
    mName = name;
    mKind = kind;
    mDisplayName = displayName;
    mAlternateXmlNames = new String[0];
  }

  /**
   * Returns the enum by its name as it appears as a folder name under {@code res/}.
   *
   * @param folderName name of the inner class of the R class, e.g. "drawable" or "color".
   */
  @Nullable
  public static ResourceType fromFolderName(@NonNull String folderName) {
    return CLASS_NAMES.get(folderName);
  }

  @Nullable
  public static ResourceType fromXmlValue(@NonNull String xmlValue) {
    if (xmlValue.equals(AndroidConstants.TAG_DECLARE_STYLEABLE)
        || xmlValue.equals(STYLEABLE.mName)) {
      return null;
    }

    if (xmlValue.equals(SAMPLE_DATA.mName)) {
      return SAMPLE_DATA;
    }

    if (xmlValue.equals(AAPT.mName)) {
      return AAPT;
    }

    if (xmlValue.equals(OVERLAYABLE.mName)) {
      return OVERLAYABLE;
    }

    if (xmlValue.equals(MACRO.mName)) {
      return MACRO;
    }
    return CLASS_NAMES.get(xmlValue);
  }

  @Nullable
  public static ResourceType fromXmlTag(@NonNull Node domNode) {
    if (!(domNode instanceof Element)) {
      return null;
    }

    Element tag = (Element) domNode;
    return fromXmlTag(
        tag,
        element -> firstNonNull(element.getLocalName(), element.getTagName()),
        Element::getAttribute);
  }

  @Nullable
  public static <T> ResourceType fromXmlTag(
      @NonNull T tag,
      @NonNull Function<T, String> nameFunction,
      @NonNull BiFunction<? super T, ? super String, String> attributeFunction) {
    String tagName = nameFunction.apply(tag);
    switch (tagName) {
      case AndroidConstants.TAG_EAT_COMMENT:
        return null;
      case AndroidConstants.TAG_ITEM:
        String typeAttribute = attributeFunction.apply(tag, AndroidConstants.ATTR_TYPE);
        if (!Strings.isNullOrEmpty(typeAttribute)) {
          return fromClassName(typeAttribute);
        } else {
          return null;
        }
      default:
        return fromXmlTagName(tagName);
    }
  }

  /**
   * Returns the enum by its name as it appears in the R class.
   *
   * @param className name of the inner class of the R class, e.g. "string" or "styleable".
   */
  @Nullable
  public static ResourceType fromClassName(@NonNull String className) {
    return CLASS_NAMES.get(className);
  }

  /**
   * Returns the enum by its name as it appears in XML as a tag name.
   *
   * @param tagName name of the XML tag, e.g. "string" or "declare-styleable".
   */
  @Nullable
  public static ResourceType fromXmlTagName(@NonNull String tagName) {
    return TAG_NAMES.get(tagName);
  }

  public static Collection<String> getClassNames() {
    return CLASS_NAMES.keySet();
  }

  /**
   * Returns a translated display name for the resource type.
   */
  @NonNull
  public String getDisplayName() {
    return mDisplayName;
  }

  /**
   * Returns true if the generated R class contains an inner class for this {@link ResourceType}.
   */
  public boolean getHasInnerClass() {
    return mKind != Kind.SYNTHETIC;
  }

  public boolean getCanBeReferenced() {
    return (mKind == Kind.REAL && this != ATTR) || this == MACRO;
  }

  /**
   * Returns true if this type is a synthetic type, such as {@link #PUBLIC}
   */
  public boolean isSynthetic() {
    return mKind == Kind.SYNTHETIC;
  }

  @Override
  @NonNull
  public String toString() {
    // Unfortunately we still have code that relies on toString() returning the aapt name.
    return getName();
  }

  /**
   * Returns the resource type name, as used by XML files.
   */
  @NonNull
  public String getName() {
    return mName;
  }

  private enum Kind {
    /**
     * These types are used both in the R and as XML tag names.
     */
    REAL,

    /**
     * Styleables are handled by aapt but don't end up in the resource table. They have an R inner
     * class (called {@code styleable}), are declared in XML (using {@code declare-styleable}) but
     * cannot be referenced from XML.
     */
    STYLEABLE,

    /**
     * Other types that are not known to aapt, but are used by tools to represent some information
     * in the resources system.
     */
    SYNTHETIC,
  }
}
